package com.eds.mall.product.service.impl;

import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.TypeReference;
import com.eds.common.utils.PageUtils;
import com.eds.common.utils.Query;
import com.eds.mall.product.dao.CategoryDao;
import com.eds.mall.product.entity.CategoryEntity;
import com.eds.mall.product.service.CategoryBrandRelationService;
import com.eds.mall.product.service.CategoryService;
import com.eds.mall.product.vo.Catalog2VO;
import com.google.common.base.Strings;
import org.apache.commons.lang.math.RandomUtils;
import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;

import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;

@Service("categoryService")
public class CategoryServiceImpl extends ServiceImpl<CategoryDao, CategoryEntity> implements CategoryService {

    @Autowired
    CategoryBrandRelationService categoryBrandRelationService;

    @Autowired
    StringRedisTemplate stringRedisTemplate;

    private static String CATEGORY_KEYS = "categorys";

    @Override
    public PageUtils queryPage(Map<String, Object> params) {
        IPage<CategoryEntity> page = this.page(
                new Query<CategoryEntity>().getPage(params),
                new QueryWrapper<CategoryEntity>()
        );

        return new PageUtils(page);
    }

    @Override
    public Long[] findCatelogPath(Long catelogId) {
        List<Long> paths = new ArrayList<>();
        List<Long> parentPath = findParentPath(catelogId, paths);
        Collections.reverse(parentPath);
        return parentPath.toArray(new Long[parentPath.size()]);

    }

    @Override
    public List<CategoryEntity> queryPageWithTree(Map<String, Object> params) {
        // 1.查询所有的商品分类信息
        List<CategoryEntity> categoryEntities = baseMapper.selectList(null);
        // 2.将商品分类信息拆解为树形结构【父子关系】
        // 第一步遍历出所有的大类  parent_cid = 0
        List<CategoryEntity> list = categoryEntities.stream()
                .filter(categoryEntity -> categoryEntity.getParentCid() == 0)
                .map(categoryEntity -> {
                    // 根据大类找到多有的小类  递归的方式实现
                    categoryEntity.setChildren(getCategoryChildrens(categoryEntity, categoryEntities));
                    return categoryEntity;
                }).sorted((entity1, entity2) -> {
                    return (entity1.getSort() == null ? 0 : entity1.getSort()) - (entity2.getSort() == null ? 0 : entity2.getSort());
                }).collect(Collectors.toList());
        // 第二步根据大类找到对应的所有的小类
        return list;

    }

    @Cacheable(value = {"category"},key = "#root.method.name", sync = true)
    @Override
    public List<CategoryEntity> listLevel1Category() {
        QueryWrapper<CategoryEntity> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("parent_cid", "0");
        System.out.println("开始查询一级目录------------------");
        System.out.println("睡眠30s:" + Thread.currentThread().getName());
        try {
            Thread.sleep(30000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        return this.list(queryWrapper);
    }

    /**
     * 跟进父编号获取对应的子菜单信息
     * @param list
     * @param parentCid
     * @return
     */
    private List<CategoryEntity> queryByParenCid(List<CategoryEntity> list,Long parentCid){
        List<CategoryEntity> collect = list.stream().filter(item -> {
            return item.getParentCid().equals(parentCid);
        }).collect(Collectors.toList());
        return collect;
    }

    /**
     * 使用 spring cache 使用 redis 进行缓存
     * 此方法与 getCatelog2JSONForRedis 对等
     * @return
     */
    @Cacheable(value = {"category"},key = "#root.method.name", sync = true)
    @Override
    public Map<String, List<Catalog2VO>> getCatelog2JSON() {

        System.out.println("开始查询数据库------------->");
        // 获取所有的分类的数据
        List<CategoryEntity> list = this.list();
        // 获取一级分类
        List<CategoryEntity> leve1Category = queryByParenCid(list, 0l);
        // 把一级分类的数据转换为Map容器 key就是一级分类的编号， value就是一级分类对应的二级分类的数据
        Map<String, List<Catalog2VO>> map = leve1Category.stream().collect(Collectors.toMap(
                key -> key.getCatId().toString()
                , value -> {
                    // 根据一级分类的编号，查询出对应的二级分类的数据
                    List<CategoryEntity> l2Catalogs = queryByParenCid(list, value.getCatId());
                    List<Catalog2VO> Catalog2VOs =null;
                    if(l2Catalogs != null){
                        Catalog2VOs = l2Catalogs.stream().map(l2 -> {
                            // 需要把查询出来的二级分类的数据填充到对应的Catelog2VO中
                            Catalog2VO catalog2VO = new Catalog2VO(l2.getParentCid().toString(), null, l2.getCatId().toString(), l2.getName());
                            // 根据二级分类的数据找到对应的三级分类的信息
                            List<CategoryEntity> l3Catelogs = queryByParenCid(list, l2.getCatId());
                            if(l3Catelogs != null){
                                // 获取到的二级分类对应的三级分类的数据
                                List<Catalog2VO.Catalog3VO> catalog3VOS = l3Catelogs.stream().map(l3 -> {
                                    Catalog2VO.Catalog3VO catalog3VO = new Catalog2VO.Catalog3VO(l3.getParentCid().toString(), l3.getCatId().toString(), l3.getName());
                                    return catalog3VO;
                                }).collect(Collectors.toList());
                                // 三级分类关联二级分类
                                catalog2VO.setCatalog3List(catalog3VOS);
                            }
                            return catalog2VO;
                        }).collect(Collectors.toList());
                    }

                    return Catalog2VOs;
                }
        ));

        return map;
    }

    /***
     * 使用 redis ，解决分布式缓存问题：击穿、穿透、雪崩
     * 此方法与getCatelog2JSON对等
     * @return
     */
    public Map<String, List<Catalog2VO>> getCatelog2JSONForRedis() {
        // 先查询redis，是否有数据存储、
        // 如果有拿去返回，如果没有进行存储
        // 属于双重判断的 第一个判断
        String catelogs = stringRedisTemplate.opsForValue().get(CATEGORY_KEYS);
        // 等于 0 说明数据库暂时没有数据
        if (Objects.equals(catelogs,"0")) {
            return null;
        }
        // 等于 null，说明没有查询过数据库
        if (Strings.isNullOrEmpty(catelogs)){
            Map<String, List<Catalog2VO>> catelog2JSONForDB = null;
            // 添加锁功能，是为了防止缓存击穿
            // 单体：synchronized (this) {...}
            // 分布式  stringRedisTemplate.opsForValue().setIfAbsent(...) 如果不存在则进行添加
            String uuid = UUID.randomUUID().toString();
            // 防止查询数据库存在问报错，而导致没有解锁的问题。所以加入超时时间
            if(stringRedisTemplate.opsForValue().setIfAbsent("catelogLock", uuid,30, TimeUnit.SECONDS)) {
                try{
                    catelog2JSONForDB = getCatelog2JSONForDB();
                    return catelog2JSONForDB;
                } finally {
                    // 解锁（原子性）
                    String srcipts = "if redis.call('get',KEYS[1]) == ARGV[1]  then return redis.call('del',KEYS[1]) else  return 0 end ";
                    // 通过Redis的lua脚本实现 查询和删除操作的原子性
                    stringRedisTemplate.execute(new DefaultRedisScript<Long>(srcipts,Long.class)
                            ,Arrays.asList("lock"),uuid);
                }
            } else {
                // 睡眠一段时间，然后重新获取内容
                // 就是循环调用
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                return getCatelog2JSON();
            }
        }
        // 获取到数据，并返回
        Map<String, List<Catalog2VO>> map = JSONObject.parseObject(catelogs, new TypeReference<Map<String, List<Catalog2VO>>>() {
        });
        return map;
    }

    /**
     * 查询出所有的二级和三级分类的数据
     * 并封装为Map<String, Catalog2VO>对象
     *
     * 进行优化
     * @return
     */
    private Map<String,List<Catalog2VO>> getCatelog2JSONForDB() {
        // 属于双重判断的 第二个判断
        String catelogJson = stringRedisTemplate.opsForValue().get(CATEGORY_KEYS);
        if ("0".equals(catelogJson)) { // 查询报错结果
            return null;
        } else if (!Strings.isNullOrEmpty(catelogJson)) { // 查询正常结果
            Map<String, List<Catalog2VO>> map = JSONObject.parseObject(catelogJson, new TypeReference<Map<String, List<Catalog2VO>>>() {
            });
            return map;
        }
        System.out.println("开始查询数据库------------->");
        // 获取所有的分类的数据
        List<CategoryEntity> list = this.list();
        // 获取一级分类
        List<CategoryEntity> leve1Category = queryByParenCid(list, 0l);
        // 把一级分类的数据转换为Map容器 key就是一级分类的编号， value就是一级分类对应的二级分类的数据
        Map<String, List<Catalog2VO>> map = leve1Category.stream().collect(Collectors.toMap(
            key -> key.getCatId().toString()
            , value -> {
                // 根据一级分类的编号，查询出对应的二级分类的数据
                List<CategoryEntity> l2Catalogs = queryByParenCid(list, value.getCatId());
                List<Catalog2VO> Catalog2VOs =null;
                if(l2Catalogs != null){
                    Catalog2VOs = l2Catalogs.stream().map(l2 -> {
                        // 需要把查询出来的二级分类的数据填充到对应的Catelog2VO中
                        Catalog2VO catalog2VO = new Catalog2VO(l2.getParentCid().toString(), null, l2.getCatId().toString(), l2.getName());
                        // 根据二级分类的数据找到对应的三级分类的信息
                        List<CategoryEntity> l3Catelogs = queryByParenCid(list, l2.getCatId());
                        if(l3Catelogs != null){
                            // 获取到的二级分类对应的三级分类的数据
                            List<Catalog2VO.Catalog3VO> catalog3VOS = l3Catelogs.stream().map(l3 -> {
                                Catalog2VO.Catalog3VO catalog3VO = new Catalog2VO.Catalog3VO(l3.getParentCid().toString(), l3.getCatId().toString(), l3.getName());
                                return catalog3VO;
                            }).collect(Collectors.toList());
                            // 三级分类关联二级分类
                            catalog2VO.setCatalog3List(catalog3VOS);
                        }
                        return catalog2VO;
                    }).collect(Collectors.toList());
                }

                return Catalog2VOs;
            }
        ));

        if (map == null || map.size() == 0) {
            // 1.添加较短时间线，是为了防止缓存穿透
            stringRedisTemplate.opsForValue().set(CATEGORY_KEYS, "0", 5, TimeUnit.SECONDS);
        } else {
            // 随机数的过期时间，是为了防止缓存雪崩
            stringRedisTemplate.opsForValue().set(CATEGORY_KEYS, JSONObject.toJSONString(map), RandomUtils.nextInt(10)+1, TimeUnit.HOURS);
        }
        return map;
    }

    /**
     * 查找该大类下的所有的小类  递归查找
     *
     * @param categoryEntity   某个大类
     * @param categoryEntities 所有的类别数据
     * @return
     */
    private List<CategoryEntity> getCategoryChildrens(CategoryEntity categoryEntity
            , List<CategoryEntity> categoryEntities) {
        List<CategoryEntity> collect = categoryEntities.stream().filter(entity -> {
            // 根据大类找到他的直属的小类
            // 注意 Long 数据比较 不在 -128 127之间的数据是 new Long() 对象
            return entity.getParentCid().equals(categoryEntity.getCatId());
        }).map(entity -> {
            // 根据这个小类递归找到对应的小小类
            entity.setChildren(getCategoryChildrens(entity, categoryEntities));
            return entity;
        }).sorted((entity1, entity2) -> {
            return (entity1.getSort() == null ? 0 : entity1.getSort()) - (entity2.getSort() == null ? 0 : entity2.getSort());
        }).collect(Collectors.toList());
        return collect;
    }


    private List<Long> findParentPath(Long catelogId, List<Long> paths) {
        paths.add(catelogId);
        CategoryEntity entity = this.getById(catelogId);
        if (entity.getParentCid() != 0) {
            findParentPath(entity.getParentCid(), paths);
        }
        return paths;
    }

}